Skip to content

refactor: split god files into domain modules + data-driven model registry - #155

Merged
ltmoerdani merged 24 commits into
ltmoerdani:mainfrom
xianhongtao:refactor/split-god-files
Aug 14, 2026
Merged

refactor: split god files into domain modules + data-driven model registry#155
ltmoerdani merged 24 commits into
ltmoerdani:mainfrom
xianhongtao:refactor/split-god-files

Conversation

@xianhongtao

Copy link
Copy Markdown
Contributor

📝 What does this change?

Splits the three god files — src/extension.ts (4653 lines), src/streaming.ts (1620) and src/goUsageTracker.ts (1510) — into domain modules (behavior-preserving), and adds a data-driven model registry so per-model wiring (transport + thinking family) lives in one table.

  • src/usage/ — Go usage domain: tracker.ts, history.ts (OpenCode CLI SQLite read/aggregation), pricing.ts, formatting.ts, dashboard.ts (status bar + usage webview + tooltip SVG), moved usage.ts / usageProfile.ts / goUsageSync.ts.
  • src/transports/ — one file per transport (chatCompletions, responses, anthropic, google) + shared engine, pure sse parser, extractors, extract, streamParts, thinkTags; contract types in src/core/transport.ts (routing in src/core/routing.ts).
  • src/provider/OpenCodeProvider class, definitions (PROVIDERS + model types), messages/tokens (conversion + token estimation), settings, visionProxy.
  • src/models/metadata, modelLimits, modelCapabilities, modelNames, pricing, metadataFetcher (models.dev cache).
  • src/commands/ — provider / agent-window / diagnostics / thinking-picker handlers; src/request/headers.ts.
  • src/core/registry.ts — data-driven MODEL_REGISTRY: rows map model-family patterns → { endpointKind, sdkPackage, thinkingFamily, vendors? }; resolveModelRouting() (transport) and thinkingFamily() both read it, so adding a model family = one row (+ optionally a thinking strategy class).
  • extension.ts is now a thin entry (~414 lines) that only wires activation + command registration; the compat barrels (streaming.ts, goUsageTracker.ts) are removed and every importer references canonical paths.

🧪 How did you test it?

  • Live (VS Code Debug host): ran the extension via F5 Extension Development Host for an extended session, calling DeepSeek V4 Flash through the refactored transport path (streaming, reasoning, tool calls) — no issues observed.
  • npm run compile — clean
  • npm test305/305 (incl. 14 new registry.test.ts cases: transport×vendor routing, thinking family, lookup mechanics)
  • npm run test-retry — 7/7 (mock-server streaming/retry E2E)
  • npm run lint — fully green (editorconfig / eslint / markdown / prettier / shell / tsc / tests)
  • npm run package — VSIX builds (105 files, 2.67 MB)

✅ Checklist

  • npm run compile passes
  • npm run lint passes
  • npm test passes
  • npm run package produces a VSIX
  • I tested it works (live DeepSeek V4 Flash via Debug host + unit suite + mock-server E2E)
  • I updated docs/CHANGELOG if needed

lint.ts spawned the extension-less node_modules/.bin shims, which ENOENTs on
Windows; route them through the shell and use the .cmd variant, matching the
staged-lint fix. Add .gitattributes enforcing LF normalization so files are
checked out with LF even with core.autocrlf=true, keeping prettier and
shellcheck green on Windows.
Add an [Unreleased] changelog entry and update the devlog (session entry,
Session Handoff, Completed History) for the per-provider thinking strategies,
single-config-authority resolution, request module split and Windows lint
fixes.
Remove the OpenCode Go/Zen Set API Key commands and the Set/Clear API Key
items in Manage Provider; keys are now configured exclusively through VS Codes
native BYOK flow (Language Models -> + Add Models). SecretStorage stays as an
internal per-vendor mirror (opencodego.apiKey / opencodezen.apiKey) that the
BYOK resolution writes so agent-host variants and cold-start requests inherit
the group key, fixing the latent collision where Go and Zen shared a single
secret and overwrote each others key. Refresh Models / Test Connection now
point at the BYOK flow when no key is configured.
# Conflicts:
#	src/streaming.ts
Copilot AI lite review requested due to automatic review settings August 14, 2026 09:03
@xianhongtao

Copy link
Copy Markdown
Contributor Author
屏幕截图 2026-08-14 170519

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR performs a large-scale refactor of the extension by splitting prior “god files” into domain-focused modules, while also introducing a data-driven MODEL_REGISTRY so model-family routing (endpoint + SDK hint) and thinking-family detection share a single source of truth.

Changes:

  • Split usage, transports, provider, models, commands, request-building, and core routing/contract logic into dedicated modules.
  • Added src/core/registry.ts model registry and updated routing + thinking-family resolution to consult it.
  • Refactored thinking into per-family strategy classes (src/thinking/) and updated request builders to consume those strategies.

Reviewed changes

Copilot reviewed 84 out of 87 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/usage/usageProfile.ts Updates config imports/exports to new shared config location.
src/usage/usage.ts Fixes formatTokenCount import path after module split.
src/usage/pricing.ts New: centralized Go usage cost estimation with live resolver + bundled fallback table.
src/usage/history.ts New: reads OpenCode CLI SQLite usage history and buckets usage for charts.
src/usage/goUsageSync.ts Updates tracker/config imports for new usage module layout.
src/usage/formatting.ts New: UI formatting helpers for usage status bar + quick pick content.
src/transports/thinkTags.ts New: streaming <think>...</think> stripping filter.
src/transports/streamParts.ts New: shared stream-part helpers incl. thinking part emission plumbing.
src/transports/sse.ts New: pure SSE data: parsing helper.
src/transports/responses.ts New: OpenAI Responses transport adapter.
src/transports/google.ts New: Google/Gemini transport adapter.
src/transports/extract.ts New: response extraction helpers + usage summary accumulation.
src/transports/engine.ts New: shared streaming engine (fetch, retries, SSE loop, summaries).
src/transports/chatCompletions.ts New: OpenAI-compatible chat-completions transport adapter.
src/transports/anthropic.ts New: Anthropic Messages transport adapter.
src/thinking/types.ts New: thinking system shared types (pure).
src/thinking/schema.ts New: schema builders for per-model thinking configuration UI.
src/thinking/resolve.ts New: resolves effective thinking settings with provenance.
src/thinking/qwen.ts New: Qwen thinking strategy implementation.
src/thinking/provider.ts New: thinking provider factory + family mapping via model registry.
src/thinking/payload.ts New: pure payload inspection (bodyRequestsThinking).
src/thinking/openai.ts New: OpenAI GPT thinking strategy implementation.
src/thinking/minimax.ts New: MiniMax thinking strategy implementation.
src/thinking/mimo.ts New: Mimo thinking strategy implementation.
src/thinking/kimi.ts New: Kimi thinking strategy implementation.
src/thinking/glm.ts New: GLM thinking strategy implementation.
src/thinking/fallback.ts New: fallback strategy for unknown families / metadata-only reasoning.
src/thinking/deepseek.ts New: DeepSeek thinking strategy implementation.
src/thinking/base.ts New: shared base class for thinking providers.
src/thinking.ts Converts legacy monolith into a public barrel re-exporting new thinking modules.
src/test/visionProxy.test.ts Updates import paths to new src/models/ layout.
src/test/usageProfile.test.ts Updates dynamic import path to new src/usage/ layout.
src/test/registry.test.ts New: unit tests for registry-based routing + thinking-family lookup.
src/test/modelNames.test.ts Updates import path to src/models/modelNames.ts.
src/test/modelLimits.test.ts Updates import path to src/models/modelLimits.ts.
src/test/metadata.test.ts Updates import path to src/models/metadata.ts.
src/test/goUsageTracker.test.ts Updates imports and module loading to new usage/* files.
src/test/goUsageSync.test.ts Updates import paths to new usage modules.
src/test/config.test.ts Adds tests for per-vendor secret-key resolution.
src/request/types.ts New: shared request/transport payload types (pure).
src/request/shared.ts New: shared request-builder helpers (image detection).
src/request/schema.ts New: sanitizes tool JSON schema for upstream compatibility.
src/request/openai.ts New: request body builders for chat-completions + Responses API.
src/request/headers.ts New: builds OpenCode request/session headers + stable hash utilities.
src/request/google.ts New: Gemini generateContent request body builder.
src/request/builders.ts New: request-builders public barrel for compatibility.
src/request/anthropic.ts New: Anthropic Messages request body builder + message conversion.
src/provider/visionProxy.ts New: vision proxy logic + quick pick configuration UI.
src/provider/tokens.ts New: token estimation + text extraction for VS Code parts.
src/provider/settings.ts New: settings + per-model schema + capabilities/limits resolution.
src/provider/messages.ts New: converts VS Code chat messages into wire messages (incl. images/tools/thinking).
src/provider/definitions.ts New: provider definitions + user agent + transient error classification.
src/models/pricing.ts New: maps models.dev cost to VS Code pricing fields and categories.
src/models/modelNames.ts New: model-id → human-friendly name formatting helpers.
src/models/modelLimits.ts Fixes config import path after refactor.
src/models/modelCapabilities.ts New: stable capabilities builder for marketplace-safe installs.
src/models/metadataFetcher.ts New: models.dev cache fetch/orchestration + globalState persistence.
src/models/metadata.ts Updates config/utils/providerTypes import paths after split.
src/core/transport.ts New: transport contract types shared by all transports.
src/core/routing.ts Refactors routing to consult core/registry.ts instead of hardcoded logic.
src/core/registry.ts New: data-driven model-family registry (endpoint + sdkPackage + thinkingFamily + vendors).
src/contextWindowHookBridge.ts Updates UsageSnapshot import path after usage module split.
src/contextWindowHook.ts Updates UsageSnapshot import path after usage module split.
src/config.ts Adds Zen secret key + secretKeyFor() vendor mapping.
src/commands/thinkingPicker.ts New: command to update global thinking effort settings.
src/commands/providers.ts New: provider-related commands (utility models + enable/disable).
src/commands/diagnostics.ts New: model picker diagnostics dump command.
src/commands/agentsWindow.ts New: agent-window support enablement/warmup utilities.
src/chatParts.ts Updates UsageSnapshot import path after usage module split.
scripts/verify-estimate-token-count.ts Updates import path to new src/models/modelLimits.ts.
scripts/validate-models.ts Updates to new thinking provider strategy + routing module paths.
scripts/staged-lint.ts Windows-safe .cmd shim handling + shell execution for spawnSync.
scripts/lint.ts Windows-safe .cmd shim handling + shell execution for spawnSync.
README.md Updates key-storage documentation and command list for new BYOK flow.
package.json Removes legacy “Set API Key” command registration.
docs/architecture/02-20260809-provider-adapter-architecture.md Updates timeline with completed refactor + registry work.
docs/architecture/01-20260514-open-code-provider-architecture.md Updates architecture notes to reflect removal of legacy key entry and new behavior.
CHANGELOG.md Adds Unreleased notes describing refactor + registry + thinking changes.
.gitattributes New: enforce LF normalization (esp. for Windows tooling consistency).
Suppressed comments (1)

src/transports/thinkTags.ts:115

  • Same as the case: the comment/doc says whitespace after <think> is trimmed, but the implementation only skips newlines. If the model emits <think> the first visible reasoning character will be a space.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/usage/history.ts
Comment on lines +149 to +160
const bucketCount = Math.round((dayStartMs - firstDay) / DAY_MS) + 1;
const buckets: UsageDayPoint[] = Array.from({ length: bucketCount }, (_, i) => ({
dayStart: firstDay + i * DAY_MS,
cost: 0,
tokens: 0,
requests: 0,
}));
const byModel = new Map<string, Map<number, ModelDayUsage>>();

const add = (model: string | undefined, timestamp: number, cost: number, tokens: number): void => {
const index = Math.round((timestamp - firstDay) / DAY_MS);
if (index < 0 || index >= bucketCount) return;
Comment on lines +86 to +92
// Skip a single leading whitespace after </think> for cleaner output
if (pos < buffer.length && (buffer[pos] === "\n" || buffer[pos] === "\r")) {
pos += 1;
if (pos < buffer.length && buffer[pos] === "\n") {
pos += 1;
}
}
@ltmoerdani

Copy link
Copy Markdown
Owner

Nice one @xianhongtao, solid refactor.

Cross-checked against docs/architecture/02-20260809-provider-adapter-architecture.md and all 5 Strangler Fig phases are done. Target folder structure matches 1:1. The registry in core/registry.ts is what we had in mind: one row per model family, routing + thinking as data not logic.

A few things before merge:

  1. Merge conflicts: this branch is CONFLICTING with main. Needs a rebase. There are 3 open PRs in the queue (fix(autocomplete): no ghost text in the Copilot Chat prompt box #154, Feat/configurable api base url #149, fix(usage): count cached tokens in every total — Codebase/Today/Yesterday were ~99% short #152) so better to let those land first, then resolve conflicts to avoid going back and forth.

  2. dashboard.ts at 1,317 lines: creeping into god-file territory again. Not a blocker now, but worth splitting later (maybe rings vs tabs as separate modules).

  3. OpenCodeProvider.ts at 1,215 lines: if the provider class keeps growing, split it up.

Scope is big (87 files, +9k/-8k) but everything is behavior-preserving and tests are green (305/305).

Merge priority: #154#149#152 → then #155.

# Conflicts:
#	src/streaming.ts
Split the 1510-line goUsageTracker.ts god file into the usage domain:
- usage/tracker.ts — GoUsageTracker class + types + time-window helpers
- usage/history.ts — OpenCode CLI SQLite history read/aggregation (pure)
- usage/pricing.ts — bundled pricing snapshot + estimateCost (pure)
- usage/formatting.ts — status-bar / quick-pick formatting

Move usage.ts / usageProfile.ts / goUsageSync.ts into src/usage/ and keep
goUsageTracker.ts as a thin barrel re-exporting the historical public API.
Behavior-preserving; all importers + tests updated.
…transport.ts

Split the 1620-line streaming.ts god file into the transport domain:
- core/transport.ts — StreamRequestOptions / TransportRequestSummary types
- transports/chatCompletions.ts / responses.ts / anthropic.ts / google.ts —
  one entry per transport (OpenAI chat / OpenAI Responses / Anthropic
  Messages / Google GenerateContent)
- transports/engine.ts — shared HTTP+SSE streaming engine + retry/backoff
- transports/sse.ts — pure SSE data-line parser
- transports/extractors.ts — Base/OpenAi/Anthropic response extractors
- transports/extract.ts — non-stream extraction + pure delta helpers
- transports/streamParts.ts — progress/thinking part emission
- transports/thinkTags.ts — pure inline <think> tag stripper

streaming.ts is kept as a thin barrel re-exporting the historical public
API. Behavior-preserving; verified with npm run test-retry (mock server).
Move healthy single-domain modules into their final homes:
- src/models/ — metadata.ts, modelLimits.ts, modelCapabilities.ts, modelNames.ts
- src/core/ — routing.ts (transport types already live in core/transport.ts)

Update every importer (extension.ts, request/*, thinking/*, transports/*,
usage/*, tests, scripts/validate-models.ts, verify-estimate-token-count.ts).
Behavior-preserving; pure path churn.
Extract ~1150 lines out of extension.ts:
- src/usage/dashboard.ts — usage state, status bar, webview panel (incl. the
  583-line HTML template), tooltip SVG builder, tracker/profile plumbing
- src/models/metadataFetcher.ts — models.dev metadata cache state + the
  clear/get/refresh orchestration (owns its own in-memory cache)

extension.ts keeps live references via exported getters/setters. Verified:
compile + 291 tests + lint green.
…tions.ts

Move the provider definition domain out of extension.ts: PROVIDERS table,
ProviderDefinition / OpenCodeModel / ModelListEntry / ModelListResponse /
ConvertedMessageResult / LanguageModelConfiguration types, plus the
getUserAgent / isTransientFetchError helpers.

extension.ts now imports from provider/definitions. Behavior-preserving;
compile + 291 tests + lint green.
Split the I partition out of extension.ts:
- provider/messages.ts — convertMessage (vscode parts → wire format, incl.
  image normalization, MiMo tool-result flattening, reasoning echo) +
  normalizeMessages / trimOldImagesFromHistoryInPlace / hasMessagePayload
- provider/tokens.ts — messageText / estimateChatMessageTokenCount /
  partToTokenCount / partToText etc.

Behavior-preserving; compile + 291 tests + lint green.
… headers

Move the remaining request-path helpers out of extension.ts:
- provider/settings.ts — modelConfigurationSchema / getSettings / modelLimits /
  modelCapabilities / shouldHideDeprecatedModel / resolveRawModelId /
  resolveVendorFromId / getConfiguredApiKey / isVisionProxyEnabled
- provider/visionProxy.ts — proxyVision + showVisionProxyPicker (whole K partition)
- models/pricing.ts — modelPricingFields + costCategory
- request/headers.ts — buildOpenCodeRequestHeaders + stringifyInitiator etc.

extension.ts is now down to ~1400 lines (from 4653). Behavior-preserving;
compile + 291 tests + lint green.
…rovider.ts

Move the 1100-line provider class out of extension.ts into its own module
(one cohesive concern). Also extract the provider command helpers
(configureUtilityModels / toggleProviderEnabled) into commands/providers.ts
which both the class and activate() import.

extension.ts is now ~630 lines (was 4653) and contains only wiring:
activate (provider registration + command handlers), the agent-window
helpers, model-picker diagnostics and the thinking-effort picker.

Behavior-preserving; compile + 291 tests + test-retry + lint green.
…kers

Move the remaining command helpers out of extension.ts into commands/:
- commands/agentsWindow.ts — ensure/revert agent-window support + warm
  model-picker metadata
- commands/diagnostics.ts — showModelPickerDiagnostics
- commands/thinkingPicker.ts — showThinkingEffortPicker

extension.ts is now ~440 lines and holds only activate() wiring + deactivate
(no business logic). Behavior-preserving; compile + 291 tests + lint green.
…aths

The split is complete — update every importer to reference the canonical
modules directly and delete the two compat barrels:
- src/streaming.ts -> src/transports/* + src/core/transport.ts
- src/goUsageTracker.ts -> src/usage/{tracker,history,pricing,formatting}.ts

No behavior change; compile + 291 tests + lint green.
Add src/core/registry.ts as the single data-driven source of truth for
per-model wiring, per the provider-adapter architecture doc:

  MODEL_REGISTRY: model-family rows -> { patterns, endpointKind,
  sdkPackage, thinkingFamily, vendors? }

- core/routing.ts resolveModelRouting() now reads the registry (first
  match wins, vendor restrictions honored) instead of an if-chain.
- thinking/provider.ts thinkingFamily() reads the registry (vendor-
  agnostic) instead of a second hardcoded prefix table.
- ModelEndpointKind type moves to core/registry.ts (re-exported by
  provider/definitions.ts).

Adding a new model family = adding ONE row (+ optionally a thinking
strategy class). Context limits/capabilities stay metadata-driven
(live models.dev) — not duplicated in a static table.

Behavior-preserving: identical routing/family outcomes, verified by 14
new registry tests + 305 total + test-retry 7/7 + lint.
@xianhongtao
xianhongtao force-pushed the refactor/split-god-files branch from 99e4b5e to 1ad1f5f Compare August 14, 2026 13:12
@ltmoerdani

Copy link
Copy Markdown
Owner

Hey @xianhongtao, thanks for this. I pulled the branch and ran it locally: compile is clean, 310/310 unit tests pass (a bit more than the 305 you listed, the registry work added a few), test-retry is 7/7 on the mock server, and eslint has nothing to complain about. I also diffed the old routing and thinking logic against the registry table line by line and the behavior lines up: GPT to Responses, Claude to Messages, minimax-m2 split by vendor, Gemini only on Zen, everything else chat-completions. The thinking payloads (deepseek effort, qwen budget, minimax adaptive, the kimi-k2.7 force-on) all match what we had before.

The structure is a clear win. extension.ts went from 4600+ lines to a thin entry point, transports are split per endpoint, usage is in its own domain folder, and adding a model family as one registry row is much easier to reason about than editing routing and thinking in two places. The tests cover the tricky bits too, like the minimax-m2 vs minimax ordering and the vendor restrictions.

One thing I want to flag before we merge, and it's around the API key change. Previously Go and Zen shared the same opencodego.apiKey secret, and this PR splits them so Zen now reads opencodezen.apiKey. That fixes the collision where one provider overwrote the other's key, which is good. But anyone who set their Zen key through the old Set API Key command has it sitting in opencodego.apiKey, and after this change Zen looks in the new slot, finds nothing, and shows zero models until they re-add via BYOK. I don't see a one-time migration in the PR. Two options: copy the legacy key into the Zen slot on activation when it's empty and Go doesn't hold a separate key, or ship this as a documented breaking change with a clear note in the release notes. I lean toward the migration since it's a few lines, but happy to hear your take.

The thinking refactor also drops the globalState shadow copy in favor of a single config authority. That's a behavior change (a good one, it stops one model's thinking effort leaking onto another) rather than a pure refactor, so it deserves a manual spin on a couple of models before release. I don't think it blocks the merge.

One more thing on release timing: given how big this is, I don't plan to ship it right after merging. I want to sit on it for a bit, run it through a longer live session across several models and both vendors, and only cut a release once I'm confident nothing regressed. The merge itself doesn't mean it goes out the door.

Merge-wise I'll keep the full commit history (merge commit, no squash) so all 24 commits stay intact.

@ltmoerdani
ltmoerdani merged commit a95565f into ltmoerdani:main Aug 14, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants